// Copyright (c) Appeon
// The following has been modified by Appeon under compliance with the Apache 2.0 license from the original work

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Data;
using System.Data.Common;
using System.Globalization;
using JetBrains.Annotations;
using SnapObjects.Commons;

namespace SnapObjects.Data
{
    public abstract class TypeMapping
    {
        /// <summary>
        ///     Gets the mapping to be used when the only piece of information is that there is a null value.
        /// </summary>
        public static readonly TypeMapping NullMapping = new NullTypeMapping("NULL");

        private class NullTypeMapping : TypeMapping
        {
            public NullTypeMapping(string dbTypeName)
                : base(dbTypeName)
            {
            }

            public override TypeMapping Clone(string dbTypeName, int? size)
            {
                return new NullTypeMapping(dbTypeName);
            }
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="RelationalTypeMapping" /> class.
        /// </summary>
        /// <param name="dbTypeName"> The name of the database type. </param>
        /// <param name="clrType"> The .NET type. </param>
        /// <param name="dbType"> The <see cref="System.Data.DbType" /> to be used. </param>
        /// <param name="unicode"> A value indicating whether the type should handle Unicode data or not. </param>
        /// <param name="size"> The size of data the property is configured to store, or null if no size is configured. </param>
        protected TypeMapping(
            [NotNull] string dbTypeName,
            [NotNull] Type clrType,
            [CanBeNull] DbType? dbType = null,
            bool unicode = false,
            int? size = 30)
            : this(dbTypeName)
        {
            Check.NotNull(clrType, nameof(clrType));

            this.ClrType = clrType;
            this.DbType = dbType;
            this.IsUnicode = unicode;
            this.Size = size;
        }

        private TypeMapping([NotNull] string dbTypeName)
        {
            Check.NotEmpty(dbTypeName, nameof(dbTypeName));

            this.DbTypeName = dbTypeName;
        }

        /// <summary>
        ///     Creates a copy of this mapping.
        /// </summary>
        /// <param name="dbTypeName"> The name of the database type. </param>
        /// <param name="size"> The size of data the property is configured to store, or null if no size is configured. </param>
        /// <returns> The newly created mapping. </returns>
        public abstract TypeMapping Clone([NotNull] string dbTypeName, int? size);

        /// <summary>
        ///     Gets the name of the database type.
        /// </summary>
        public virtual string DbTypeName { get; }

        /// <summary>
        ///     Gets the .NET type.
        /// </summary>
        public virtual Type ClrType { get; }

        /// <summary>
        ///     Gets the <see cref="System.Data.DbType" /> to be used.
        /// </summary>
        public virtual DbType? DbType { get; }

        /// <summary>
        ///     Gets a value indicating whether the type should handle Unicode data or not.
        /// </summary>
        public virtual bool IsUnicode { get; }

        /// <summary>
        ///     Gets the size of data the property is configured to store, or null if no size is configured.
        /// </summary>
        public virtual int? Size { get; }

        /// <summary>
        ///     Gets the string format to be used to generate SQL literals of this type.
        /// </summary>
        protected virtual string SqlLiteralFormatString { get; } = "{0}";

        /// <summary>
        ///     Creates a <see cref="DbParameter" /> with the appropriate type information configured.
        /// </summary>
        /// <param name="command"> The command the parameter should be created on. </param>
        /// <param name="name"> The name of the parameter. </param>
        /// <param name="value"> The value to be assigned to the parameter. </param>
        /// <param name="nullable"> A value indicating whether the parameter should be a nullable type. </param>
        /// <returns> The newly created parameter. </returns>
        public virtual DbParameter CreateParameter(
            [NotNull] DbCommand command,
            [NotNull] IAdoDbParameter adoDbParameter)
        {
            Check.NotNull(command, nameof(command));

            var parameter = command.CreateParameter();
            parameter.Direction = adoDbParameter.Direction;
            parameter.ParameterName = adoDbParameter.ParameterName;

            if (parameter.Direction != ParameterDirection.ReturnValue)
            {
                parameter.Value = adoDbParameter.Value ?? DBNull.Value;
            }

            parameter.IsNullable = adoDbParameter.Nullable;

            if (this.DbType.HasValue)
            {
                parameter.DbType = this.DbType.Value;
            }

            if (this.Size.HasValue)
            {
                parameter.Size = this.Size.Value;
            }

            if (parameter.Direction != ParameterDirection.Input &&
                adoDbParameter is AdoDbParameter dbParameter)
            {
                parameter.Precision = dbParameter.Precision;
                parameter.Scale = dbParameter.Scale;
            }

            this.ConfigureParameter(parameter, adoDbParameter);

            return parameter;
        }

        /// <summary>
        ///     Configures type information of a <see cref="DbParameter" />.
        /// </summary>
        /// <param name="parameter"> The parameter to be configured. </param>
        protected virtual void ConfigureParameter([NotNull] DbParameter parameter, IAdoDbParameter adoDbParameter)
        {
            if (parameter.Direction == ParameterDirection.ReturnValue &&
                this.DbType != null)
            {
                parameter.DbType = this.DbType.Value;
            }
        }

        /// <summary>
        ///     Generates the SQL representation of a literal value.
        /// </summary>
        /// <param name="value">The literal value.</param>
        /// <returns>
        ///     The generated string.
        /// </returns>
        public virtual string GenerateSqlLiteral([CanBeNull] object value)
        {
            return value == null
                           ? "NULL"
                           : this.GenerateNonNullSqlLiteral(value);
        }

        /// <summary>
        ///     Generates the SQL representation of a non-null literal value.
        /// </summary>
        /// <param name="value">The literal value.</param>
        /// <returns>
        ///     The generated string.
        /// </returns>
        protected virtual string GenerateNonNullSqlLiteral([NotNull] object value)
        {
            return String.Format(CultureInfo.InvariantCulture, this.SqlLiteralFormatString, Check.NotNull(value, nameof(value)));
        }

        public virtual object ChangeDbType(object dbValue)
        {
            if (Convert.IsDBNull(dbValue) || dbValue == null)
            {
                return null;
            }
            else if (dbValue.GetType() == this.ClrType)
            {
                return dbValue;
            }
            else
            {
                return Convert.ChangeType(dbValue, this.ClrType);
            }
        }

        public virtual bool AssignCompatiable(Type assignedType)
        {
            var dbType = TypeHelper.UnwrapEnumType(TypeHelper.UnwrapNullableType(this.ClrType));
            var modelType = TypeHelper.UnwrapEnumType(TypeHelper.UnwrapNullableType(assignedType));

            return this.IsAssignable(modelType, dbType);
        }

        public virtual bool IsStrictAssignable(Type modelType)
        {
            var dbType = TypeHelper.UnwrapEnumType(TypeHelper.UnwrapNullableType(this.ClrType));
            var genericModelType = TypeHelper.UnwrapEnumType(TypeHelper.UnwrapNullableType(modelType));

            return dbType == genericModelType ||
                        genericModelType == typeof(object);
        }

        protected virtual bool IsAssignable(Type modelType, Type dbType)
        {
            return dbType == modelType ||
                modelType == typeof(object);
        }
    }
}
